<?php
namespace aop;

use Closure, Exception;

class AOPConstructor {

    use \aop\core\AbstractConcretOperationTrait,
        \aop\core\MethodClosureCreationTrait,
        \aop\core\MethodDocumentInterpreterTrait,
        \aop\core\ConfigAbstractManagerTrait;

    private $_biz_name = '';
    private $_class_name = '';
    private $_method_name = '';
    private $_param_name_list = [];
    private $_config_abstracts = [];

    private static $_execute_flag_before = 'before';
    private static $_execute_flag_replace = 'replace';
    private static $_execute_flag_interpret = 'interpret';
    private static $_execute_flag_after = 'after';

    /**
     * @param  [string]         $bizName     [业务配置名称]
     * @param  [string]         $abstract    [类标识：类的命令空间+类名]
     * @param  [object]         $concret     [类的实例]
     * @param  [string]         $methodName  [执行方法的名称]
     * @param  [array]          $params      [执行对象方法需要传入的参数集合]
     */
    public function execute($bizName, $abstract, $concret, $methodName, array $params = array()) {
        if (!is_string($bizName) || !$bizName || !is_string($methodName) || !$methodName) {
            throw new \Exception('empty parameter biz_name or method_name');
        }

        $classConcretMap = $this->_fetchClassnameAndConcret($abstract, $concret);

        $this->_biz_name = $bizName;
        $this->_method_name = $methodName;
        $this->_class_name = $classConcretMap['class_name'];
        $instance = $classConcretMap['instance'];

        $executeClosure = $this->fetchCallbackClosure($instance);
        return $executeClosure($instance, $params);
    }

    private function fetchCallbackClosure($instance) {
        $closure = $this->_hasMethodClosure($this->_class_name, $this->_method_name);
        if ($closure instanceof \Closure) {
            return $closure;
        }

        $this->_config_abstracts = $this->_fetchAbstractConfig($this->_biz_name);
        if (!$this->_config_abstracts) {
            return $this->_createClosureWithoutDocument($this->_class_name, $this->_method_name);
        }

        $methodReflectionData = $this->_fetchMethodReflectionInfo($instance, $this->_method_name);
        $methodDocument = $methodReflectionData['document'];
        $methodReflection = $methodReflectionData['reflection'];
        $this->_param_name_list = $methodReflectionData['param_name_list'];

        if (!$methodDocument) {
            return $this->_createClosureWithoutDocument($this->_class_name, $this->_method_name);
        }

        $documentRules = $this->_interpretMethodDocument($methodDocument);
        if (!$documentRules) {
            return $this->_createClosureWithoutDocument($this->_class_name, $this->_method_name);
        }

        return $this->createClosureWithDocument($methodReflection, $documentRules);
    }

    private function createClosureWithDocument($methodReflection, $documentRules) {
        $beforeClosure = $this->createBindingEventClosure(self::$_execute_flag_before, $documentRules);
        $afterClosure = $this->createBindingEventClosure(self::$_execute_flag_after, $documentRules);
        $interpretClosure = $this->createBindingEventClosure(self::$_execute_flag_interpret, $documentRules);
        $replaceClosure = $this->createReplaceClosure($documentRules);

        $closure = function($instance, array $args) use ($methodReflection, $beforeClosure, $afterClosure, $replaceClosure, $interpretClosure) {
            $beforeClosure instanceof Closure && $beforeClosure($args);

            $result = NULL;
            $isReplace = FALSE;

            if ($replaceClosure instanceof Closure) {
                $replaceResult = $replaceClosure($args);
                if (is_array($replaceResult)) {
                    isset($replaceResult['success']) && $isReplace = $replaceResult['success'];
                    isset($replaceResult['data']) && $result = $replaceResult['data'];
                }
            }

            if (!$isReplace) {
                $methodReflection->setAccessible(TRUE);
                $result = $methodReflection->invokeArgs($instance, $args);
                $interpretClosure instanceof Closure && $interpretClosure($args, $result);
            }

            $afterClosure instanceof Closure && $afterClosure($args, $result, $isReplace);

            return $result;
        };
        $this->_saveMethodClosure($this->_class_name, $this->_method_name, $closure);
        return $closure;
    }

    private function createBindingEventClosure($index, $documentRules) {
        $callbackMap = isset($documentRules[$index]) ? $documentRules[$index] : [];
        if (!$callbackMap) {
            return NULL;
        }

        $closureParam = $this->_fetchEventClosureParam();
        $closureParam['execute_flag'] = $index;
        $closureParam['param_name_list'] = $this->_param_name_list;

        return function(array $args, $methodResult = NULL, $isReplace = FALSE) use ($callbackMap, $closureParam) {
            $closureParam['method_param'] = $args;
            $closureParam['method_result'] = $methodResult;
            $closureParam['is_replace'] = $isReplace;
            foreach ($callbackMap as $_callback) {
                $closureParam['param_rule'] = $_callback['param_rule'];
                $this->executeConfigAbstractClosure([$_callback['abstract'], $_callback['method']], $closureParam);
            }
        };
    }

    private function createReplaceClosure($documentRules) {
        $replaceCallbackMap = isset($documentRules[self::$_execute_flag_replace]) ? array_shift($documentRules[self::$_execute_flag_replace]) : [];
        if (!$replaceCallbackMap) {
            return NULL;
        }

        $closureParam = $this->_fetchEventClosureParam();
        $closureParam['execute_flag'] = self::$_execute_flag_replace;
        $closureParam['param_rule'] = $replaceCallbackMap['param_rule'];
        $closureParam['param_name_list'] = $this->_param_name_list;

        return function (array $args) use ($replaceCallbackMap, $closureParam) {
            $closureParam['method_param'] = $args;
            return $this->executeConfigAbstractClosure([$replaceCallbackMap['abstract'], $replaceCallbackMap['method']], $closureParam);
        };
    }

    private function executeConfigAbstractClosure($callback, $callbackParams) {
        try {
            list($abstract, $method) = $callback;
            $closure = $this->_hasConfigAbstractMethodClosure($this->_biz_name, $abstract, $method);
            if ($closure instanceof \Closure) {
                return $closure($callbackParams);
            }

            $concret = $this->_fetchAbstractConfigConcret($this->_biz_name, $abstract, $method, $this->_config_abstracts);
            if (!is_object($concret)) {
                return NULL;
            }

            $closure = function($params) use ($concret, $method) {
                return call_user_func_array([$concret, $method], [$params]);
            };
            $this->_saveConfigAbstractMethodClosure($this->_biz_name, $abstract, $method, $closure);
            return $closure($callbackParams);
        } catch (\Exception $e) {
            return NULL;
        }
    }
}